/**
 * TODO: Vložit licenci.
 */

package cz.enif.commander;

import java.util.List;

import cz.enif.commander.sources.ISource;
import cz.enif.commander.sources.SourceFactory;
import cz.enif.commander.values.IValue;

/**
* Commander.
* 
* <pre>
* script :: command { command-separator command } .
* 
* command-separator :: ';' | new-line .
* 
* command :: command-identifier { argument } .
* 
* command-identifier :: identifier | string-literal .
* 
* identifier :: non-digit { non-dogit | digit } .
* 
* argument :: tuple .
* 
* tuple :: value { '/' [ value ] } [ '/' value ] .
* 
* value ::
*   boolean-literal |
*   integer-number |
*   double-number |
*   string-literal .
* 
* boolean-literal :: "true" | "false" .
* 
* integer-number :: [ sign ] digit { digit } .
* 
* double-number :: 
*   [ sign ] digit-sequence [ fractional-part ] [ scale-factor ] .
* 
* fractional-part :: '.' digit-sequence .
* 
* scale-factor :: ( 'e' | 'E' ) [ scale-factor-sign ]  digit-sequence .
* 
* digit-sequence :: digit { digit } .
* 
* scale-factor-sign :: sign .
* 
* sign :: '+' | '-' .
* 
* digit :: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' .
* 
* non-digit :: '_' | 'a' .. 'z' | 'A' .. 'Z' .
* 
* string-literal :: string-terminator { char } string-terminator .
* 
* string-terminator :: '"' || ''' .
* 
* 
* Commands
* 
*   puts { values }
* </pre>
*
* @author Přemysl Fára
*/
public class Commander implements IError
{
   //// CONSTRUCTORS ////

   public Commander()
   {
       this.interpreter = new Interpreter();
       
       this.clearError();
       
       // commands
       this.registerCommands();
   }

   //// PUBLIC ////

   /**
    * Nahraje, přeloží a spustí script uložený v souboru.
    *
    *  @param scriptFileName Jméno souboru obsahující script.
    *
    *  @return True, pokud vše proběhlo v pořádku, jinak false.
    */
   	public boolean runFile(String scriptFileName)
	{
   		ISource source = SourceFactory.getFileSource();
    	if (source.init(scriptFileName) == false)
    	{
    		return false;
    	}
    	
    	boolean result = this.runSource(source);
    	
    	source.close();
    	
    	return result;
	}

   	/**
     * Nahraje, přeloží a spustí script uložený v řetězci.
     *
     *  @param script Řetězec obsahující script.
     *
     *  @return True, pokud vše proběhlo v pořádku, jinak false.
     */
    public boolean runString(String script)
    {
    	ISource source = SourceFactory.getStringSource();
    	if (source.init(script) == false)
    	{
    		return false;
    	}
    	
    	boolean result = this.runSource(source);
    	
    	source.close();
    	
    	return result;
    }
   
    /**
     * Přeloží a spustí script uložený v zadaném zdroji.
     * Zdroj není po zkonzumování uzavřen. To musí provézt
     * voloající.
     *
     *  @param source Zdroj (inicializovaný) obsahující script.
     *
     *  @return True, pokud vše proběhlo v pořádku, jinak false.
     */
    public boolean runSource(ISource source)
    {
    	if (source == null)
    	{
    		this.setError("Nezadán zdroj.");
    		
    		return false;
    	}
    	
    	// Připojíme zdroj.
    	if (this.setSource(source) == false)
    	{
    		return false;
    	}
    	
    	while (this.runCommand())
   		{
   			if (this.wasLastCommand()) break;
   		}

    	// V případě chyby předáme příznak. 
   		if (this.wasError()) return false;
   		
   		return true;
    }
    
    /**
     * Připraví zdroj typu soubor pro čtení parserem.
     *
     *  @param scriptFileName Jméno souboru obsahující script.
     *
     *  @return True, pokud vše proběhlo v pořádku, jinak false.
     */
    public boolean setSourceFromFile(String scriptFileName)
    {
    	ISource source = SourceFactory.getFileSource();
    	if (source.init(scriptFileName) == false)
    	{
    		return false;
    	}
    	
    	return this.setSource(source);
    }
    
    /**
     * Připraví zdroj typu řetězec pro čtení parserem.
     *
     *  @param script Řetězec obsahující script.
     *
     *  @return True, pokud vše proběhlo v pořádku, jinak false.
     */
    public boolean setSourceFromString(String script)
    {
    	ISource source = SourceFactory.getStringSource();
    	if (source.init(script) == false)
    	{
    		return false;
    	}
    	
    	return this.setSource(source);
    }
    
    /**
     * Připraví zdroj pro čtení parserem.
     * 
     * @param source Zdroj, který má být použit pro čtení scriptu.
     *   Nesmí být null. Pokud je již nějaký zdroj nastaven, je
     *   uzavřen.
     *   
     * @return True při úspěchu, jinak false.
     */
    public boolean setSource(ISource source)
    {
    	return this.interpreter(this.interpreter.setSource(source));
    }
    
    /**
     * Přeloží celý skript.
     * 
     * Zdroj musí být zadán přez setSource(source).
     * 
     * @return True při úspěchu, jinak false.
     */
    public List<Command> compile()
    {
    	List<Command> commands = this.interpreter.compile();
    	if (this.interpreter.wasError())
    	{
    		this.setError(this.interpreter.getErrorMessage());
    		
    		return null;
    	}
    	
    	return commands;
    }
    
    /**
     * Spustí přeložený script.
     *
     *  @param commands Seznam instrukcí programu, který se má provést.
     *
     *  @return True, pokud vše proběhlo v pořádku, jinak false.
     */
    public boolean run(List<Command> commands)
    {
    	return this.interpreter(this.interpreter.run(commands));
    }
    
    /**
     * Provede jeden příkaz ze zdroje. Zdroj musí být nastaven
     * pomocí setSource().
     * 
     * Pomocí wasEOF() jde zjitit, zda již byl zpracován poslední příkaz.
     * Při volání po zpracování posledního příkazu dojde k chybě. 
     * 
     * @return True při úspěchu, jinak false.
     */
    public boolean runCommand()
    {
    	return this.interpreter(this.interpreter.runCommand());
    }
    
    //// COMMANDS ////
    
    /**
     * Definuje nový příkaz se jménem commandName, případně změní již definovanému
     * příkazu jeho implementaci.
     *
     * @param commandName Jméno nově definovaného příkazu. Povinný parametr.
     * @param command Odkaz na implementaci příkazu. Povinný parametr.
     *
     * @return True při úspěchu, jinak false.
     */
    public boolean addCommand(String commandName, IUserCommand command)
    {
    	return this.interpreter(this.interpreter.addCommand(commandName, command));
    }
    
    /**
     * Zjistí, zda je definován příkaz se zadaným jménem.
     *
     * @param commandName Jméno příkazu.
     *
     * @return True, pokud příkaz se zadaným jménem existuje, jinak false.
     */
    public boolean haveCommand(String commandName)
    {
    	return this.interpreter.haveCommand(commandName);
    }
    
    /**
     * Vrací příkaz se jménem commandName, nebo null, 
     * pokud není zadáno commandName, či neexistuje příkaz 
     * se zadaným jménem.
     * 
     * Neexistuje-li příkaz daného jména, jde o chybu. 
     * Existenci příkazu je možné ověřit voláním haveCommand(...).
     *
     * @param commandName Jméno příkazu, který chceme získat.
     *
     * @return Odkaz na třídu implementující příkaz zadaného jména, nebo null,
     *   pokud takový příkaz neexistuje.
     */
    public IUserCommand getCommand(String commandName)
    {
    	return this.interpreter.getCommand(commandName);
    }
    
    /**
     * Vrací defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     * 
     * @return Defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     */
    public IUserCommand getDefaultCommand()
    {
    	return this.interpreter.getDefaultCommand();
    }
    
    /**
     * Nastaví defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     * 
     * @param defaultCommand defaultní příkaz, který se vykoná pro všechny nedefinované příkazy.
     */
    public void setDefaultCommand(IUserCommand defaultCommand)
    {
   		this.interpreter.setDefaultCommand(defaultCommand);
    }
    
    /**
     * Vrací jméno posledně volaného, resp. právě běžícího, příkazu. 
     * 
     * @return Jméno posledně volaného, resp. právě běžícího, příkazu.
     */
    public String getLastCalledCommandName()
    {
    	return this.interpreter.getLastCalledCommandName();
    }
    
    //// STATE ////
    
    /**
     * Vrací true, pokud byl přečten a zpracován poslední příkaz.
     * 
     * @return True, pokud byl přečten a zpracován poslední příkaz.
     */
    public boolean wasLastCommand()
    {
    	return this.interpreter.wasLastCommand();
    }
    
    //// ERRORS ////
    
    /* (non-Javadoc)
	 * @see cz.enif.commander.IError#wasError()
	 */
	@Override
	public boolean wasError() 
	{
		return this.wasError;
	}

	/* (non-Javadoc)
	 * @see cz.enif.commander.IError#getErrorMessage()
	 */
	@Override
	public String getErrorMessage() 
	{
		return this.errorMessage;
	}
    
    //// PRIVATE ////

    private Interpreter interpreter;

    private boolean wasError;
    private String errorMessage;
    
    /**
     * Vymaže popis a příznak chyby.
     */
    private void clearError()
    {
    	this.wasError = false;
    	this.errorMessage = "OK";
    }
    
    /**
     * Nastaví příznak chyby a její popis.
     * 
     * @param message Popis chyby.
     */
    private void setError(String message)
    {
    	this.wasError = true;
    	this.errorMessage = (message == null ? "ERROR" : message);
    }
    
    /**
     * V případě chyby v interpreteru ji zkopíruje a vrátí result.
     * 
     * @param result Výsledek volání, který se má vrátit.
     * 
     * @return Vrací předaný result.
     */
    private boolean interpreter(boolean result)
    {
    	if (this.interpreter.wasError())
    	{
    		this.setError(this.interpreter.getErrorMessage());
    	}
    	
    	return result;
    }
    
    //// BUILD-IN COMMANDS ////

    private void registerCommands()
   	{
    	this.interpreter.addCommand("puts", new IUserCommand()
    	{
    		public boolean exec(List<IValue> params)
    		{
    			return cmdPuts(params);
    		}
           
    		@Override
    		public String toString()
    		{
    			return "puts { value }";
    		}
    	});
   	}

    // puts { value }
    // (value1, ..., valuen, args-count -- )
    // (args-count -- )
    private boolean cmdPuts(List<IValue> params)
    {
    	// echo
    	if (params == null || params.size() == 0)
    	{
    		Logger.println(null);

    		return true;
    	}

    	// echo arg1 .. argn
    	for (IValue v : params)
    	{
    		Logger.print(v.toString());
    	}

    	return true;
    }
}
